Skip to content

Developer control for the control-plane IP family policy - #339

Merged
Ryanmello07 merged 1 commit into
urnetwork:mainfrom
Ryanmello07:upstream/ip-family
Sep 3, 2026
Merged

Developer control for the control-plane IP family policy#339
Ryanmello07 merged 1 commit into
urnetwork:mainfrom
Ryanmello07:upstream/ip-family

Conversation

@Ryanmello07

Copy link
Copy Markdown
Contributor

The iOS half of the control-plane IPv4/IPv6 policy. Depends on urnetwork/sdk#152, which depends on urnetwork/connect#207.

Why

Some users' ISPs route badly to the service's Hurricane Electric IPv6 address. The path connects and then stalls in TLS — invisible to Happy Eyeballs, which races only the TCP handshake. connect learns and routes around it; this is the manual override and the visibility.

What this adds

A tri-state Control connections row in the Developer screen, cycling Automatic → Force IPv4 → Force IPv6, modelled on the log-verbosity row beside it including its read-back discipline: the value shown is what the SDK reports, never what was last tapped, so a set that was clamped or refused is visible rather than assumed.

One thing it deliberately does not copy from that row. The verbosity row is inert without a device — there is nothing to set a level on. This row does the opposite: it reads and writes the process-global policy, so it works signed out and with the tunnel down. Those are exactly the states a user is in when the API is unreachable, which is the only reason to reach for it. A device-gated row would be inert in the situation it exists to rescue.

IpFamily.swift is pure vocabulary — labels, detail text, clamp, cycle order — so it is testable with no device. IpFamilyState.swift holds the state and the one write path, which prefers the device (reaching the extension over RPC when the tunnel is up) and falls back to the network space, then the process-global setter.

The detail line reports what Automatic has learned, e.g. "Automatic — IPv6 demoted for 4m (2 strikes)". That status comes from device?.getControlIpFamilyStatus(), not the app process: with the tunnel up the extension is the process that dials and therefore the one that learns, so an app-local read is blind precisely where the heuristic fires.

Integration note

PacketTunnelProvider.swift had moved upstream since the fork base. Our change there is comment-only — a KNOWN LIMITATION block recording why the extension's first connect after a force can still dial under Auto (the extension restores from its own container, and the app's first sync arrives ~0.5–1.5 s after the RPC listener opens). Integrated alongside your changes, nothing clobbered.

That limitation was assessed and deliberately not fixed: the demotion ledger is in-memory with no persistence, so a fresh extension process starts empty on every tunnel start and an unforced user re-learns the bad family each connect. Closing the window would buy one repetition of a cost the unforced path pays every time, at the price of a second, staleable source of truth beside LocalState.

Verification

xcodebuild build (generic/platform=iOS) and the networkTests suite both pass — ** BUILD SUCCEEDED ** / ** TEST SUCCEEDED **, exit 0.

Also installed and exercised on a physical iPhone (Release, iOS 27): the row is live signed out and with the tunnel down, cycles correctly, and a force survives a full app restart.

⚠️ One local-environment note, not a defect in this PR: a from-scratch full-scheme build needs a fresh urnetwork/sdk checkout, because WidgetSnapshotWriter.swift (an upstream-only file) references SdkGetColorHex and SdkOrderConnectedProviderLocations, which a stale sibling SDK build lacks.

🤖 Generated with Claude Code

https://claude.ai/code/session_014vN7zh8WeQYCtirhcA3aWw

api.bringyour.com and connect.bringyour.com publish AAAA records in
Hurricane Electric space that some ISPs route badly. Happy Eyeballs
self-heals a pre-connect blackhole, but an HE tunnel that passes the
small SYN/SYN-ACK and then drops the large TLS ServerHello makes IPv6
win the race and stall after connecting. The sdk now demotes a family
proven to fail that way; this adds the Developer Tools row (Auto /
Force IPv4 / Force IPv6) that surfaces and overrides it on iOS.

- IpFamily: pure policy-to-label/clamp/cycle helpers, mirroring the
  sdk's IpFamilyPolicy constants.
- IpFamilyState: the device/networkSpace/process-global three-way
  write path (matching android), with an in-flight guard around the
  write-then-read-back so a second tap can't be silently lost.
- DeveloperView: a "Control connections" row that works with no
  device and with the tunnel down, since that's when it's needed.
- PacketTunnelProvider: records the known limitation that the first
  connect after a tunnel-down force can still dial under Auto, and
  why that's an acceptable, bounded cost rather than a bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vN7zh8WeQYCtirhcA3aWw
@Ryanmello07
Ryanmello07 merged commit 534a84d into urnetwork:main Sep 3, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant